using System.Diagnostics;
using Community.CsharpSqlite.Entity;

namespace Community.CsharpSqlite
{
    using sqlite3_stmt = Vdbe;

    public partial class Sqlite3
    {
        /*
    ** 2003 April 6
    **
    ** The author disclaims copyright to this source code.  In place of
    ** a legal notice, here is a blessing:
    **
    **    May you do good and not evil.
    **    May you find forgiveness for yourself and forgive others.
    **    May you share freely, never taking more than you give.
    **
    *************************************************************************
    ** This file contains code used to implement the VACUUM command.
    **
    ** Most of the code in this file may be omitted by defining the
    ** SQLITE_OMIT_VACUUM macro.
    *************************************************************************
    **  Included in SQLite3 port to C#-SQLite;  2008 Noah B Hart
    **  C#-SQLite is an independent reimplementation of the SQLite software library
    **
    **  SQLITE_SOURCE_ID: 2010-03-09 19:31:43 4ae453ea7be69018d8c16eb8dabe05617397dc4d
    **
    **  $Header: Community.CsharpSqlite/src/vacuum_c.cs,v 6604176a7dbe 2010/03/12 23:35:36 Noah $
    *************************************************************************
    */
        //#include "sqliteInt.h"
        //#include "vdbeInt.h"

#if !SQLITE_OMIT_VACUUM && !SQLITE_OMIT_ATTACH
        /*
** Finalize a prepared statement.  If there was an error, store the
** text of the error message in *pzErrMsg.  Return the result code.
*/

        private static int vacuumFinalize(sqlite3 db, sqlite3_stmt pStmt, string pzErrMsg)
        {
            int rc;
            rc = VdbeAux.VdbeFinalize(pStmt);
            if (rc != 0)
            {
                Malloc.SetString(ref pzErrMsg, db, sqlite3_errmsg(db));
            }
            return rc;
        }

        /*
** Execute zSql on database db. Return an error code.
*/

        private static int execSql(sqlite3 db, string pzErrMsg, string zSql)
        {
            sqlite3_stmt pStmt = null;
#if !NDEBUG
      int rc;
      //VVA_ONLY( int rc; )
#endif
            if (zSql == null)
            {
                return StatusCode.SQLITE_NOMEM;
            }
            string Dummy = null;
            if (StatusCode.SQLITE_OK != sqlite3_prepare(db, zSql, -1, ref pStmt, ref Dummy))
            {
                Malloc.SetString(ref pzErrMsg, db, sqlite3_errmsg(db));
                return sqlite3_errcode(db);
            }
#if !NDEBUG
      rc = sqlite3_step( pStmt );
      //VVA_ONLY( rc = ) sqlite3_step(pStmt);
      Debug.Assert( rc != StatusCode.SQLITE_ROW );
#else
            sqlite3_step(pStmt);
#endif
            return vacuumFinalize(db, pStmt, pzErrMsg);
        }

        /*
    ** Execute zSql on database db. The statement returns exactly
    ** one column. Execute this as SQL on the same database.
    */

        private static int execExecSql(sqlite3 db, string pzErrMsg, string zSql)
        {
            sqlite3_stmt pStmt = null;
            int rc;

            string Dummy = null;
            rc = sqlite3_prepare(db, zSql, -1, ref pStmt, ref Dummy);
            if (rc != StatusCode.SQLITE_OK) return rc;

            while (StatusCode.SQLITE_ROW == sqlite3_step(pStmt))
            {
                rc = execSql(db, pzErrMsg, sqlite3_column_text(pStmt, 0));
                if (rc != StatusCode.SQLITE_OK)
                {
                    vacuumFinalize(db, pStmt, pzErrMsg);
                    return rc;
                }
            }

            return vacuumFinalize(db, pStmt, pzErrMsg);
        }

        /*
    ** The non-standard VACUUM command is used to clean up the database,
    ** collapse free space, etc.  It is modelled after the VACUUM command
    ** in PostgreSQL.
    **
    ** In version 1.0.x of SQLite, the VACUUM command would call
    ** gdbm_reorganize() on all the database tables.  But beginning
    ** with 2.0.0, SQLite no longer uses GDBM so this command has
    ** become a no-op.
    */

        private static void sqlite3Vacuum(Parse pParse)
        {
            Vdbe v = SelectHelper.GetVdbe(pParse);
            if (v != null)
            {
                VdbeAux.VdbeAddOp2(v, OPCode.OP_Vacuum, 0, 0);
            }
            return;
        }

        /*
    ** This routine implements the OPCode.OP_Vacuum opcode of the VDBE.
    */

        private static int sqlite3RunVacuum(ref string pzErrMsg, sqlite3 db)
        {
            int rc = StatusCode.SQLITE_OK; /* Return code from service routines */
            Btree pMain; /* The database being vacuumed */
            Btree pTemp; /* The temporary database we vacuum into */
            string zSql = ""; /* SQL statements */
            int saved_flags; /* Saved value of the db.flags */
            int saved_nChange; /* Saved value of db.nChange */
            int saved_nTotalChange; /* Saved value of db.nTotalChange */
            dxTrace saved_xTrace; //void (*saved_xTrace)(void*,const char*);  /* Saved db->xTrace */
            Db pDb = null; /* Database to detach at end of vacuum */
            bool isMemDb; /* True if vacuuming a :memory: database */
            int nRes;

            if (0 == db.autoCommit)
            {
                Malloc.SetString(ref pzErrMsg, db, "cannot VACUUM from within a transaction");
                return StatusCode.SQLITE_ERROR;
            }

            /* Save the current value of the database flags so that it can be 
      ** restored before returning. Then set the writable-schema flag, and
      ** disable CHECK and foreign key constraints.  */
            saved_flags = db.flags;
            saved_nChange = db.nChange;
            saved_nTotalChange = db.nTotalChange;
            saved_xTrace = db.xTrace;
            db.flags |= Flag.SQLITE_WriteSchema | Flag.SQLITE_IgnoreChecks;
            db.flags &= ~(Flag.SQLITE_ForeignKeys | Flag.SQLITE_ReverseOrder);

            db.xTrace = null;

            pMain = db.aDb[0].pBt;
            isMemDb = sqlite3PagerIsMemdb(sqlite3BtreePager(pMain));

            /* Attach the temporary database as 'vacuum_db'. The synchronous pragma
      ** can be set to 'off' for this file, as it is not recovered if a crash
      ** occurs anyway. The integrity of the database is maintained by a
      ** (possibly synchronous) transaction opened on the main database before
      ** sqlite3BtreeCopyFile() is called.
      **
      ** An optimisation would be to use a non-journaled pager.
      ** (Later:) I tried setting "PRAGMA vacuum_db.journal_mode=OFF" but
      ** that actually made the VACUUM run slower.  Very little journalling
      ** actually occurs when doing a vacuum since the vacuum_db is initially
      ** empty.  Only the journal header is written.  Apparently it takes more
      ** time to parse and run the PRAGMA to turn journalling off than it does
      ** to write the journal header file.
      */
            if (sqlite3TempInMemory(db))
            {
                zSql = "ATTACH ':memory:' AS vacuum_db;";
            }
            else
            {
                zSql = "ATTACH '' AS vacuum_db;";
            }
            rc = execSql(db, pzErrMsg, zSql);
            if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;
            pDb = db.aDb[db.nDb - 1];
            Debug.Assert(db.aDb[db.nDb - 1].zName == "vacuum_db");
            pTemp = db.aDb[db.nDb - 1].pBt;

            /* The call to execSql() to attach the temp database has left the file
      ** locked (as there was more than one active statement when the transaction
      ** to read the schema was concluded. Unlock it here so that this doesn't
      ** cause problems for the call to BtreeSetPageSize() below.  */
            sqlite3BtreeCommit(pTemp);

            nRes = sqlite3BtreeGetReserve(pMain);

            /* A VACUUM cannot change the pagesize of an encrypted database. */
#if SQLITE_HAS_CODEC
if( db.nextPagesize ){
extern void sqlite3CodecGetKey(sqlite3*, int, void**, int*);
int nKey;
char *zKey;
sqlite3CodecGetKey(db, 0, (void**)&zKey, nKey);
if( nKey ) db.nextPagesize = 0;
}
#endif

            if (sqlite3BtreeSetPageSize(pTemp, sqlite3BtreeGetPageSize(pMain), nRes, 0) != 0
                || (!isMemDb && sqlite3BtreeSetPageSize(pTemp, db.nextPagesize, nRes, 0) != 0)
                //|| UnitTest.NEVER( db.mallocFailed != 0 )
                )
            {
                rc = StatusCode.SQLITE_NOMEM;
                goto end_of_vacuum;
            }
            rc = execSql(db, pzErrMsg, "PRAGMA vacuum_db.synchronous=OFF");
            if (rc != StatusCode.SQLITE_OK)
            {
                goto end_of_vacuum;
            }

#if !SQLITE_OMIT_AUTOVACUUM
            sqlite3BtreeSetAutoVacuum(pTemp, db.nextAutovac >= 0
                                                 ? db.nextAutovac
                                                 : sqlite3BtreeGetAutoVacuum(pMain));
#endif

            /* Begin a transaction */
            rc = execSql(db, pzErrMsg, "BEGIN EXCLUSIVE;");
            if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;

            /* Query the schema of the main database. Create a mirror schema
      ** in the temporary database.
      */
            rc = execExecSql(db, pzErrMsg,
                             "SELECT 'CREATE TABLE vacuum_db.' || substr(sql,14) " +
                             "  FROM sqlite_master WHERE type='table' AND name!='sqlite_sequence'" +
                             "   AND rootpage>0"
                );
            if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;
            rc = execExecSql(db, pzErrMsg,
                             "SELECT 'CREATE INDEX vacuum_db.' || substr(sql,14)" +
                             "  FROM sqlite_master WHERE sql LIKE 'CREATE INDEX %' ");
            if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;
            rc = execExecSql(db, pzErrMsg,
                             "SELECT 'CREATE UNIQUE INDEX vacuum_db.' || substr(sql,21) " +
                             "  FROM sqlite_master WHERE sql LIKE 'CREATE UNIQUE INDEX %'");
            if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;

            /* Loop through the tables in the main database. For each, do
      ** an "INSERT INTO vacuum_db.xxx SELECT * FROM main.xxx;" to copy
      ** the contents to the temporary database.
      */
            rc = execExecSql(db, pzErrMsg,
                             "SELECT 'INSERT INTO vacuum_db.' || quote(name) " +
                             "|| ' SELECT * FROM main.' || quote(name) || ';'" +
                             "FROM main.sqlite_master " +
                             "WHERE type = 'table' AND name!='sqlite_sequence' " +
                             "  AND rootpage>0"
                );
            if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;

            /* Copy over the sequence table
      */
            rc = execExecSql(db, pzErrMsg,
                             "SELECT 'DELETE FROM vacuum_db.' || quote(name) || ';' " +
                             "FROM vacuum_db.sqlite_master WHERE name='sqlite_sequence' "
                );
            if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;
            rc = execExecSql(db, pzErrMsg,
                             "SELECT 'INSERT INTO vacuum_db.' || quote(name) " +
                             "|| ' SELECT * FROM main.' || quote(name) || ';' " +
                             "FROM vacuum_db.sqlite_master WHERE name=='sqlite_sequence';"
                );
            if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;


            /* Copy the triggers, views, and virtual tables from the main database
      ** over to the temporary database.  None of these objects has any
      ** associated storage, so all we have to do is copy their entries
      ** from the SQLITE_MASTER table.
      */
            rc = execSql(db, pzErrMsg,
                         "INSERT INTO vacuum_db.sqlite_master " +
                         "  SELECT type, name, tbl_name, rootpage, sql" +
                         "    FROM main.sqlite_master" +
                         "   WHERE type='view' OR type='trigger'" +
                         "      OR (type='table' AND rootpage=0)"
                );
            if (rc != 0) goto end_of_vacuum;

            /* At this point, unless the main db was completely empty, there is now a
      ** transaction open on the vacuum database, but not on the main database.
      ** Open a btree level transaction on the main database. This allows a
      ** call to sqlite3BtreeCopyFile(). The main database btree level
      ** transaction is then committed, so the SQL level never knows it was
      ** opened for writing. This way, the SQL transaction used to create the
      ** temporary database never needs to be committed.
      */
            {
                uint meta = 0;
                int i;

                /* This array determines which meta meta values are preserved in the
        ** vacuum.  Even entries are the meta value number and odd entries
        ** are an increment to apply to the meta value after the vacuum.
        ** The increment is used to increase the schema cookie so that other
        ** connections to the same database will know to reread the schema.
        */
                var aCopy = new byte[]
                                {
                                    BtreeMeta.BTREE_SCHEMA_VERSION, 1, /* Add one to the old schema cookie */
                                    BtreeMeta.BTREE_DEFAULT_CACHE_SIZE, 0, /* Preserve the default page cache size */
                                    BtreeMeta.BTREE_TEXT_ENCODING, 0, /* Preserve the text encoding */
                                    BtreeMeta.BTREE_USER_VERSION, 0, /* Preserve the user version */
                                };

                Debug.Assert(sqlite3BtreeIsInTrans(pTemp));
                Debug.Assert(sqlite3BtreeIsInTrans(pMain));

                /* Copy Btree meta values */
                for (i = 0; i < Utility.ArraySize(aCopy); i += 2)
                {
                    /* GetMeta() and UpdateMeta() cannot fail in this context because
          ** we already have page 1 loaded into cache and marked dirty. */
                    sqlite3BtreeGetMeta(pMain, aCopy[i], ref meta);
                    rc = sqlite3BtreeUpdateMeta(pTemp, aCopy[i], (meta + aCopy[i + 1]));
                    if (UnitTest.NEVER(rc != StatusCode.SQLITE_OK)) goto end_of_vacuum;
                }

                rc = sqlite3BtreeCopyFile(pMain, pTemp);
                if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;
                rc = sqlite3BtreeCommit(pTemp);
                if (rc != StatusCode.SQLITE_OK) goto end_of_vacuum;
#if !SQLITE_OMIT_AUTOVACUUM
                sqlite3BtreeSetAutoVacuum(pMain, sqlite3BtreeGetAutoVacuum(pTemp));
#endif
            }
            Debug.Assert(rc == StatusCode.SQLITE_OK);
            rc = sqlite3BtreeSetPageSize(pMain, sqlite3BtreeGetPageSize(pTemp), nRes, 1);

            end_of_vacuum:
            /* Restore the original value of db.flags */
            db.flags = saved_flags;
            db.nChange = saved_nChange;
            db.nTotalChange = saved_nTotalChange;
            db.xTrace = saved_xTrace;

            /* Currently there is an SQL level transaction open on the vacuum
      ** database. No locks are held on any other files (since the main file
      ** was committed at the btree level). So it safe to end the transaction
      ** by manually setting the autoCommit flag to true and detaching the
      ** vacuum database. The vacuum_db journal file is deleted when the pager
      ** is closed by the DETACH.
      */
            db.autoCommit = 1;

            if (pDb != null)
            {
                sqlite3BtreeClose(ref pDb.pBt);
                pDb.pBt = null;
                pDb.pSchema = null;
            }

            Build.ResetInternalSchema(db, 0);

            return rc;
        }
#endif
        // * SQLITE_OMIT_VACUUM && SQLITE_OMIT_ATTACH */
    }
}